﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Web.UI.WebControls;
using System.Linq.Expressions;
using System.Reflection;
using System.Collections;
using System.Data.Linq.Mapping;

namespace GoodStuff.Data.Linq
{
    /// <summary>
    /// Extension set to provide dynamic linq sort operations
    /// </summary>
    public static class SortExtenions
    {
        /// <summary>
        /// Allow the OrderBy or OrderbyDescending to be based on a property instead of a method.
        /// </summary>
        /// <typeparam name="TSource"></typeparam>
        /// <typeparam name="TKey"></typeparam>
        /// <param name="source"></param>
        /// <param name="keySelector"></param>
        /// <param name="ascending"></param>
        /// <returns></returns>
        public static IOrderedQueryable<TSource> OrderBy<TSource, TKey>(this IQueryable<TSource> source, Expression<Func<TSource, TKey>> keySelector, bool ascending)
        {
            if (ascending)
            {
                return source.OrderBy(keySelector);
            }
            else
            {
                return source.OrderByDescending(keySelector);
            }
        }

        /// <summary>
        /// Allow the ThenBy or ThenBybyDescending to be based on a property instead of a method.
        /// </summary>
        /// <typeparam name="TSource"></typeparam>
        /// <typeparam name="TKey"></typeparam>
        /// <param name="source"></param>
        /// <param name="keySelector"></param>
        /// <param name="ascending"></param>
        /// <returns></returns>
        public static IOrderedQueryable<TSource> ThenBy<TSource, TKey>(this IOrderedQueryable<TSource> source, Expression<Func<TSource, TKey>> keySelector, bool ascending)
        {
            if (ascending)
            {
                return source.ThenBy(keySelector);
            }
            else
            {
                return source.ThenByDescending(keySelector);
            }
        }

        /// <summary>      
        /// Extension method to sort dynamically based on a attribute referenced by a string. Should be performed at one the last steps while retrieving
        /// data from the database. When sorted is based on a property that doesn't exist in the mappingTable of the referenced LINQ-type, 
        /// the IQueryable will be broken. Sorting will take place in memory in that case after the data is directly retrieved.
        /// </summary>      
        /// <typeparam name="T">The type of the IQueryable collection we are working with.</typeparam>      
        /// <param name="source">The source collection.</param>      
        /// <param name="attribute">The name of the attribute.</param>      
        /// <param name="ascending">SortDirection to apply. ascending = true, descending = false</param>
        /// will be sorted in stead of all items from the resultset. You know at that point that there will not be more items</param>
        /// <returns></returns>
        public static IQueryable<T> GenericSort<T>(this IQueryable<T> source, string attribute, bool ascending)
        {
            PropertyInfo property = null;
            //Property can be hidden in subobject. Example : Gemeente.Provincie.Naam

            string[] propSplitted = attribute.Split(new char[] { '.' }, StringSplitOptions.RemoveEmptyEntries);
            if (propSplitted.Length == 1)
            {
                property = typeof(T).GetProperty(attribute);
            }
            else
            {
                int depth = 1;
                foreach (string prop in propSplitted)
                {
                    if (depth == 1)
                    {
                        property = typeof(T).GetProperty(prop);
                    }
                    else
                    {
                        property = property.PropertyType.GetProperty(prop);
                    }
                    depth++;
                }
            }
            

            //Property can be hidden in subobject. Example : Gemeente.Provincie.Naam
            if (property != null)
            {
                //There are 2 types of properties
                //1) Properties that are persistent within the database, by LINQ
                //   --> For these properties, we are able to perform SQL sorting (IQueryable)
                //2) Custom defined properties defined in an extra partial class (based on a generated partial class by LINQ). 
                //      Example 'FirstName' and 'Surname' exists in the database. But 'Fullname' is created for easily combining the name-elements
                //   --> For these properties, we have to perform sorting in memory. We execute a ToList() to execute the query.
                //
                //Custom defined properties don't have a System.Data.Linq.Mapping.ColumnAttribute
                if (property.IsDefined(typeof(ColumnAttribute), false) == false)
                {
                    //perform operation in memory. Retrieve the data from the database with the ToList()
                    source = source.ToList().AsQueryable(); //continue as IQueryable
                }
                else
                {
                    //We have a property that is handled by LINQ through Mapping. There are a couple of datatypes
                    //that can't by sorted by LINQ directly in the database through a IQueryable. In that case we perform the sorting in memory.
                    //We do a ToList() to execute the databasequery and retrieve the data.
                    ColumnAttribute columnAttribute = (ColumnAttribute)property.GetCustomAttributes(typeof(System.Data.Linq.Mapping.ColumnAttribute), false).FirstOrDefault();

                    string dbType = columnAttribute.DbType; //Example "NText" or "NText NOT NULL"
                    if (!string.IsNullOrEmpty(dbType))
                    {
                        dbType = dbType.ToUpper();
                        if (dbType.Contains("TEXT")) //NText + Text
                        {
                            source = source.ToList().AsQueryable();
                        }
                    }
                }
            }
            else
            {
                throw new ArgumentOutOfRangeException("attribute", string.Format("Can't find a property '{0}' to sort on for objecttype {1}", attribute, typeof(T)));
            }            
            Type genericSorterType = typeof(GenericSorter<,>).MakeGenericType(typeof(T), property.PropertyType);

            ISorter<T> sorter = (ISorter<T>)Activator.CreateInstance(genericSorterType);

            return sorter.Sort(source, attribute, ascending);
        }

        /// <summary>
        /// Helper interface : ISorter
        /// </summary>
        /// <typeparam name="T"></typeparam>
        public interface ISorter<T>
        {
            /// <summary>
            /// Sorts a dataset
            /// </summary>
            /// <param name="source">the source collection</param>
            /// <param name="attribute">the name of the property to sort on</param>
            /// <param name="ascending">the sortdirection. ascending = true, descending = false</param>
            /// <returns>A sorted resultset</returns>
            IQueryable<T> Sort(IQueryable<T> source, string attribute, bool ascending);
        }

        /// <summary>
        /// A Genericsorter which sorts datasets based on the given property name and given sortdirection
        /// </summary>
        /// <typeparam name="T">The Type of object we're dealing with.</typeparam>
        /// <typeparam name="PT">The Type of the property we're sorting on.</typeparam>
        public class GenericSorter<T, PT> : ISorter<T>
        {
            /// <summary>
            /// Sorts the dataset
            /// </summary>
            /// <param name="source">the source collection</param>
            /// <param name="attribute">the name of the property to sort on</param>
            /// <param name="ascending">the sortdirection. ascending = true, descending = false</param>
            /// <returns>A sorted resultset</returns>
            public IQueryable<T> Sort(IQueryable<T> source, string attribute, bool ascending)
            {
                var param = Expression.Parameter(typeof(T), "x");

                Expression expr = param;
                foreach (string prop in attribute.Split(new char[] {'.'}, StringSplitOptions.RemoveEmptyEntries))
                {
                    // use reflection (not ComponentModel) to mirror LINQ   
                    expr = Expression.PropertyOrField(expr, prop);
                }
                var sortLambda = Expression.Lambda<Func<T, PT>>(Expression.Convert(expr, typeof(PT)), param);

                switch (ascending)
                {
                    case true:
                        {
                            return source.OrderBy<T, PT>(sortLambda);
                        }
                    default:
                        {
                            return source.OrderByDescending<T, PT>(sortLambda);
                        }
                }
            }
        }
    }
}
